package processor;

import java.math.RoundingMode;
import java.util.Date;
import java.util.EnumMap;
import java.util.List;

import javax.persistence.LockModeType;

import models.Loan;
import models.Order;
import models.OrderDetail;
import models.Pay;
import models.Pay.Distribution;
import models.Pay.Status;
import models.PayDetail;
import models.Transaction.Type;
import prefix.common.SystemActor;
import prefix.platform.Constants;
import prefix.transaction.BasicTransaction;
import prefix.transaction.TransactionFactory;
import prefix.util.CalculationException;
import prefix.util.DateUtil;
import prefix.util.JPAUtil;
import prefix.util.OverallUtil;
import prefix.util.Timeable;
import repository.ActorRepository;
import repository.LoanRepository;
import repository.OrderRepository;
import repository.PayRepository;
import service.ActorService;
import service.LoanService;
import service.OrderDetailService;
import service.exception.PayException;
import service.exception.PayException.CODE;
import service.impl.LoanServiceImpl;
import service.impl.OrderDetailServiceImpl;
import service.impl.PayServiceImpl;

public class PayProcessor extends PayServiceImpl {

    public PayProcessor(Pay pay) {
        super(pay);
    }

    public PayProcessor() {

    }

    public void paymentNotify() throws PayException {
        // todo: send email notice. 
        try {
            JPAUtil.beginTransaction();
            this.dbpay.setStatus(Pay.Status.NOTIFIED);
            this.dbpay.setUpdateD(new Date());
            this.dbpay.setPullAmount(this.dbpay.getDueAmount());
            this.dbpay.setPullD(this.dbpay.getDueD());
            JPAUtil.em().merge(this.dbpay);
            JPAUtil.commitTransaction();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            throw new PayException(CODE.UNKNOWN);
        } finally {
            JPAUtil.closeEM();
        }
    }

    public void paymentPull() throws PayException {
        try {
            JPAUtil.beginTransaction();
            double dueAmount = this.dbpay.getDueAmount();
            double pullAmount = this.dbpay.getPullAmount();
            pullAmount = Math.max(dueAmount, pullAmount);
            this.dbpay.setPullAmount(pullAmount);
            this.dbpay.setStatus(Status.CURRENT);
            LoanService loanService = LoanRepository.getLoanById(this.dbpay.getLoanId());
            // add next period payment
            buildNextPayment(loanService);
            loanService.getDbloan().setCurrentPay(this.dbpay.getId());
            JPAUtil.em().merge(this.dbpay);
            JPAUtil.commitTransaction();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            throw new PayException(CODE.UNKNOWN);
        } finally {
            JPAUtil.closeEM();
        }
    }

    private void buildNextPayment(LoanService loanService) {
        long normalizedDueDate = DateUtil.normalizeDay(dbpay.getDueD()).getTime();
        long lastPeriodDate = DateUtil.normalizeDay(
                DateUtil.addMonths(loanService.getDbloan().getCreateD(), loanService.getDbloan().getDuration()))
                .getTime();
        if (normalizedDueDate < lastPeriodDate) {
            Pay futurePay = loanService.getNextPay(this.dbpay);
            JPAUtil.em().persist(futurePay);
            loanService.getDbloan().setFuturePay(futurePay.getId());
        }
    }

    public void processPayment(boolean force) throws PayException, CalculationException {
        LoanService loanService = null;
        try {
            JPAUtil.beginTransaction();
            Loan loan = JPAUtil.em().find(Loan.class, this.dbpay.getLoanId(), LockModeType.PESSIMISTIC_WRITE);
            loanService = new LoanServiceImpl(loan);
            if (!loanService.hasBeenIssued())
                throw new PayException(CODE.INVALID_LOAN_STATUS);
            if (!loanService.isValidLoanStateForProcess())
                throw new PayException(CODE.INVALID_LOAN_STATUS);
            if (!OverallUtil.isIn(this.dbpay.getStatus(), Pay.getOpenStatusList()))
                throw new PayException(CODE.INVALID_PAY_STATUS);

            ActorService actorService = ActorRepository.getActorServiceById(loan.getAid());

            double balance = actorService.getAvailableCash();

            double amountToDispurse = calculateAmountToDistribute(actorService, balance, loanService);

            if (amountToDispurse > balance)
                throw new PayException(CODE.INVALID_BALANCE_TO_DISPUSE);

            EnumMap<Distribution, Double> dist = this.waterFallDistribution(amountToDispurse, loan.getRpa());
            if (dist.get(Distribution.AmountToDistribute) <= 0 && dist.get(Distribution.PrincipalUnpaidBalance) > 0)
                executeLatePayment(loanService);
            else {
                double amountToFullPay = loanService.getAmountToBeFullyPaid();
                this.disbursePayment(loanService, dist, amountToFullPay, actorService);
            }
            this.validateDisbursementAtLpay();
            JPAUtil.em().merge(this.dbpay);
            JPAUtil.commitTransaction();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            JPAUtil.closeEM();
        }

        try {
            setLoanStatusAfterPayment(loanService);
            loanService.getProcessor().validateLoanAndPayments();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

    public static Pay.Status getLateLengthPayPeriod(Date dueDate, Date potentialReceiveDate) {
        long normalizedReceivedDate = DateUtil.normalizeDay(potentialReceiveDate).getTime();
        long normalizedDueDate = DateUtil.normalizeDay(dueDate).getTime();
        long endGracePeriod = DateUtil.addRegularDays(dueDate, Constants.GRACE_PERIOD_DAYS).getTime();
        long end31Period = DateUtil.normalizeDay(DateUtil.addMonths(dueDate, 1)).getTime() - 3 * Timeable.DAY;

        // Passed delinquent, you are INCOLLECITONS
        if (normalizedReceivedDate > end31Period)
            return Pay.Status.LATE_30_90;

        // Passed grace, you are delinquent!
        if (normalizedReceivedDate > endGracePeriod && normalizedReceivedDate <= end31Period)
            return Pay.Status.LATE_7_30;

        if (normalizedReceivedDate > normalizedDueDate && normalizedReceivedDate <= endGracePeriod)
            return Pay.Status.GRACE_PERIOD;

        if (normalizedDueDate == normalizedReceivedDate)
            return Pay.Status.CURRENT;

        return null;
    }

    private void executeLatePayment(LoanService registerLoan) throws PayException {
        Status pendingStatus = getLateLengthPayPeriod(this.dbpay.getDueD(), new Date());
        if (pendingStatus == null)
            throw new PayException(CODE.INVALID_PAY_STATUS);

        switch (pendingStatus) {
        case GRACE_PERIOD:
            this.dbpay.setStatus(Status.GRACE_PERIOD);
            break;
        case LATE_7_30:
            this.dbpay.setStatus(Status.LATE_7_30);
            this.chargeLateFeesIfNotCharged(registerLoan);
            break;
        case LATE_30_90:
            this.dbpay.setStatus(Status.LATE_30_90);
            this.chargeLateFeesIfNotCharged(registerLoan);
            break;
        case CURRENT:
            break;
        default:
            throw new PayException(CODE.INVALID_PAY_STATUS);
        }

    }

    private void chargeLateFeesIfNotCharged(LoanService loan) throws PayException {
        if (loan.getDbloan().getStatus().equals(Loan.Status.DEFAULTED))
            throw new PayException(CODE.INVALID_LOAN_STATUS);

        long normalizedDueDate = DateUtil.normalizeDay(this.dbpay.getDueD()).getTime();
        long normalizedToday = DateUtil.normalizeDay(new Date()).getTime();
        int numberOfDays = (int) ((normalizedToday - normalizedDueDate) / Timeable.DAY);

        double unpaidAmount = StrictMath.max(
                loan.getMonthlyPayment() + this.dbpay.getManagementFeePaid() - this.dbpay.getReceivedAmount(), 0);
        double lateFeesAccr = numberOfDays * (unpaidAmount * Constants.PAYMENT_LATE_FEE_PERCENT);
        lateFeesAccr = OverallUtil.round2Decimal(lateFeesAccr, RoundingMode.HALF_DOWN).doubleValue();

        this.dbpay.setLateFeeAccr(lateFeesAccr);

    }

    public void setLoanStatusAfterPayment(LoanService loanService) throws PayException {
        long normalizedReceivedDate = DateUtil.normalizeDay(new Date()).getTime();
        long normalizedDueDate = DateUtil.normalizeDay(this.dbpay.getDueD()).getTime();
        if (normalizedDueDate > normalizedReceivedDate)
            return;
        try {
            JPAUtil.beginTransaction();
            Loan.Status loanStatus = loanService.getLoanStatusAfterPayment();
            loanService.getDbloan().setStatus(loanStatus);
            loanService.getDbloan().setStatusD(new Date());
            JPAUtil.em().merge(loanService.getDbloan());
            JPAUtil.commitTransaction();
        } catch (Exception e) {
            throw new PayException(CODE.UNKNOWN, e);
        } finally {
            JPAUtil.closeEM();
        }
    }

    public void validateDisbursementAtLpay() throws CalculationException {
        // Lpay Data --
        double receivedAmnt = this.dbpay.getReceivedAmount();
        double dispursedAmnt = this.dbpay.getPrncpPaid() + this.dbpay.getIntPaid() + this.dbpay.getLateFeePaid()
                + this.dbpay.getManagementFeePaid();

        OverallUtil.validateSensitivity(receivedAmnt - dispursedAmnt, Constants.SENSITIVITY_ERROR_CENT);

    }

    private void disbursePayment(LoanService registeredLoan, EnumMap<Distribution, Double> dist,
            double amountToFullPay, ActorService borrowerService) throws PayException {
        if (dist.get(Distribution.AmountToDistribute).doubleValue() > amountToFullPay)
            throw new PayException(PayException.CODE.LARGE_AMOUNT_TO_DISPURSE);

        if (dist.get(Distribution.AmountToDistribute) <= 0)
            throw new PayException(PayException.CODE.INVALID_BALANCE_TO_DISPUSE);

        double commitAmount = registeredLoan.getDbloan().getAmount();

        if (this.dbpay.getReceivedD() == null)
            this.dbpay.setReceivedD(new Date());
        double receivedAmount = this.dbpay.getReceivedAmount() + dist.get(Distribution.AmountToDistribute)
                + dist.get(Distribution.brMgmtPaid);
        this.dbpay.setReceivedAmount(receivedAmount);

        double brManagementFee = this.dbpay.getManagementFeePaid() + dist.get(Distribution.brMgmtPaid);
        this.dbpay.setManagementFeePaid(brManagementFee);

        double lateFee = this.dbpay.getLateFeePaid() + dist.get(Distribution.LateFeesPaid);
        this.dbpay.setLateFeePaid(lateFee);

        double interestsToPay = this.dbpay.getIntPaid() + dist.get(Distribution.InterestPaid);
        this.dbpay.setIntPaid(interestsToPay);

        double prncpToPay = this.dbpay.getPrncpPaid() + dist.get(Distribution.PrincipalPaid);
        this.dbpay.setPrncpPaid(prncpToPay);
        Integer borrowerId = borrowerService.getDBActor().getId();
        Date now = new Date();
        Integer loanId = registeredLoan.getDbloan().getId();
        if (dist.get(Distribution.brMgmtPaid) > 0) {
            BasicTransaction t5 = TransactionFactory.createTransaction(Type.T5_BORROWER_PAY_MANAGEMENT_FEE, borrowerId,
                    borrowerId, SystemActor.BORROWER_MGMT_FEE_W3.getId(), null, null,
                    dist.get(Distribution.brMgmtPaid), now, null, loanId);
            JPAUtil.em().persist(t5.getDbTrans());
        }
        List<OrderDetail> detailList = OrderRepository.getOrderDetailByLoanAndStatus(this.dbpay.getLoanId(),
                OrderDetail.Status.ISSUED);
        for (OrderDetail detail : detailList) {
            OrderDetailService detailService = new OrderDetailServiceImpl(detail);
            double dispurseToLender = detailService.getLenderPortion(dist.get(Distribution.AmountToDistribute),
                    commitAmount);
            if (dispurseToLender > 0) {
                Order order = detail.getOrder();
                BasicTransaction t8 = TransactionFactory.createTransaction(Type.T8_BORROWER_MTHLY_PAY_TO_LENDER,
                        borrowerId, borrowerId, order.getAid(), null, null, dispurseToLender, now, order.getId(),
                        loanId);
                JPAUtil.em().persist(t8.getDbTrans());

                // add pay id in transaction table later

                double fractionLateFess = (dist.get(Distribution.LateFeesPaid) * dispurseToLender)
                        / dist.get(Distribution.AmountToDistribute);
                double fractionInterest = (dist.get(Distribution.InterestPaid) * dispurseToLender)
                        / dist.get(Distribution.AmountToDistribute);
                double fractionPrincipal = (dist.get(Distribution.PrincipalPaid) * dispurseToLender)
                        / dist.get(Distribution.AmountToDistribute);

                double serviceFeePerLender = calcLenderFees(fractionInterest, registeredLoan.getDbloan()
                        .getServiceFee());
                if (serviceFeePerLender > 0) {
                    BasicTransaction t6 = TransactionFactory.createTransaction(Type.T6_LENDER_PAY_SERVICE_FEE,
                            order.getAid(), order.getAid(), SystemActor.SERVICE_FEE_ACCOUNT_W4.getId(), null, null,
                            serviceFeePerLender, now, detail.getOrderId(), loanId);
                    JPAUtil.em().persist(t6.getDbTrans());
                }
                double diff = detail.getAmount() - detail.getReceivedPrncp() - fractionPrincipal;
                if (diff < 0) {
                    if (Math.abs(diff) > Constants.PRINCIPAL_REMAINING_SENSITIVITY_ERROR) {
                        throw new PayException(PayException.CODE.INVALID_DISTRIBUTION);
                    }
                    fractionPrincipal += diff;
                    fractionInterest -= diff;

                    if (fractionPrincipal < 0 || fractionInterest < 0)
                        throw new PayException(PayException.CODE.INVALID_DISTRIBUTION);
                }

                double fractionPrincipal2 = dispurseToLender - fractionLateFess - fractionInterest;

                if (Math.abs(fractionPrincipal - fractionPrincipal2) > Constants.SENSITIVITY_ERROR)
                    throw new PayException(PayException.CODE.INVALID_DISTRIBUTION);

                PayDetail payDetail = new PayDetail();
                payDetail.setCreateD(now);
                payDetail.setInterest(fractionInterest);
                payDetail.setPayId(this.dbpay.getId());
                payDetail.setLateFee(fractionLateFess);
                payDetail.setPrincipal(fractionPrincipal);
                payDetail.setLenderFee(serviceFeePerLender);
                payDetail.setStatus(PayDetail.Status.PENDING);
                payDetail.setLoanId(this.dbpay.getLoanId());
                payDetail.setTransId(t8.getDbTrans().getId());
                JPAUtil.em().persist(payDetail);
            }

        }

        if (dist.get(Distribution.PrincipalUnpaidBalance) == 0)
            this.dbpay.setStatus(Pay.Status.CLOSE_PAID);

    }

    public static double calcLenderFees(double perLender, double serviceFeeRate) {
        double servicefee = Math.max(OverallUtil.round2Decimal(serviceFeeRate * perLender, RoundingMode.CEILING)
                .doubleValue(), 0.01);
        if (perLender == 0)
            servicefee = 0;
        return servicefee;
    }

    private double calculateAmountToDistribute(ActorService actor, double availableCash, LoanService loanService) {

        double dueAmount = Math.max(this.dbpay.getPullAmount(), this.dbpay.getDueAmount());
        double amountToDispurse = dueAmount - this.dbpay.getReceivedAmount();
        amountToDispurse = Math.max(amountToDispurse, 0);
        amountToDispurse = OverallUtil.round2Decimal(amountToDispurse).doubleValue();

        double toFullyPay = loanService.getAmountToBeFullyPaid();
        double maxCanPay = Math.min(availableCash, toFullyPay);
        double needToPay = amountToDispurse;

        amountToDispurse = Math.min(maxCanPay, needToPay);

        return amountToDispurse;

    }

    private EnumMap<Distribution, Double> waterFallDistribution(double amountToDispurse, double RPA)
            throws CalculationException, PayException {
        double leftToDistribute = amountToDispurse;

        double brMgmtToPay = this.dbpay.getManagementFeeAccr() - this.dbpay.getManagementFeePaid();
        double brMgmtPaid = StrictMath.min(leftToDistribute, brMgmtToPay);
        double brMgmtUnpaidBal = brMgmtToPay - brMgmtPaid;
        leftToDistribute -= brMgmtPaid;

        // For lenders Now!
        double latefeesToPay = this.dbpay.getLateFeeAccr() - this.dbpay.getLateFeePaid();
        double latefeesPaid = StrictMath.min(leftToDistribute, latefeesToPay);
        double latefeesUnpaidBal = latefeesToPay - latefeesPaid;
        leftToDistribute -= latefeesPaid;

        double interestToPay = this.dbpay.getIntAccr() - this.dbpay.getIntPaid();
        double interestPaid = StrictMath.min(leftToDistribute, interestToPay);
        double interestUnpaidBal = interestToPay - interestPaid;
        leftToDistribute -= interestPaid;

        double accruedPrincipal = RPA - this.dbpay.getIntAccr() - this.dbpay.getPrncpPaid();
        double principalToPay = accruedPrincipal;
        principalToPay = StrictMath.min(this.dbpay.getPrncpOut(), principalToPay);
        double principalPaid = leftToDistribute;
        double principalUnpaidBal = StrictMath.max(0, principalToPay - principalPaid);

        double newOutstandingPrincipal = this.dbpay.getPrncpOut() - principalPaid;
        newOutstandingPrincipal = OverallUtil.validate(newOutstandingPrincipal, Constants.SENSITIVITY_ERROR_CENT, "");
        validatePositiveNumbers(brMgmtUnpaidBal, latefeesUnpaidBal, interestUnpaidBal, principalUnpaidBal);
        double amountToDistribute2 = principalPaid + interestPaid + latefeesPaid + brMgmtPaid;

        if (Math.abs(amountToDistribute2 - amountToDispurse) > Constants.SENSITIVITY_ERROR)
            throw new PayException(CODE.INVALID_DISTRIBUTION);

        if (newOutstandingPrincipal < principalUnpaidBal)
            throw new PayException(CODE.INVALID_DISTRIBUTION);

        double receivedAmnt = this.dbpay.getReceivedAmount();
        double dispursedAmnt = this.dbpay.getPrncpPaid() + this.dbpay.getIntPaid() + this.dbpay.getManagementFeePaid()
                + this.dbpay.getLateFeePaid();

        if (Math.abs(receivedAmnt - dispursedAmnt) > Constants.SENSITIVITY_ERROR_CENT)
            throw new PayException(CODE.INVALID_DISTRIBUTION);

        EnumMap<Distribution, Double> dist = new EnumMap<Distribution, Double>(Distribution.class);

        dist.put(Distribution.brMgmtPaid, OverallUtil.validate(brMgmtPaid, Constants.SENSITIVITY_ERROR, ""));
        dist.put(Distribution.brMgmtBalance, OverallUtil.validate(brMgmtUnpaidBal, Constants.SENSITIVITY_ERROR, ""));

        dist.put(Distribution.LateFeesPaid, OverallUtil.validate(latefeesPaid, Constants.SENSITIVITY_ERROR, ""));
        dist.put(Distribution.LateFeesUnpaidBalance,
                OverallUtil.validate(latefeesUnpaidBal, Constants.SENSITIVITY_ERROR, ""));

        dist.put(Distribution.InterestPaid, OverallUtil.validate(interestPaid, Constants.SENSITIVITY_ERROR, ""));
        dist.put(Distribution.InterestUnpaidBalance,
                OverallUtil.validate(interestUnpaidBal, Constants.SENSITIVITY_ERROR, ""));

        dist.put(Distribution.PrincipalPaid, OverallUtil.validate(principalPaid, Constants.SENSITIVITY_ERROR, ""));
        dist.put(Distribution.PrincipalUnpaidBalance,
                OverallUtil.validate(principalUnpaidBal, Constants.SENSITIVITY_ERROR, ""));
        dist.put(Distribution.PrincipalNewOutstanding,
                OverallUtil.validate(newOutstandingPrincipal, Constants.SENSITIVITY_ERROR, ""));

        dist.put(Distribution.AmountToDistribute,
                OverallUtil.validate(amountToDispurse - brMgmtPaid, Constants.SENSITIVITY_ERROR, ""));

        return dist;
    }

    private void validatePositiveNumbers(double... values) throws PayException {
        for (double d : values) {
            if (d < -Constants.SENSITIVITY_ERROR)
                throw new PayException(PayException.CODE.INVALID_DISTRIBUTION);
        }
    }

}
